/**
 *    Copyright 2009-2018 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.yinsin.jpabatis.executor.loader;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;

import com.yinsin.jpabatis.config.Configuration;
import com.yinsin.jpabatis.executor.BaseExecutor;
import com.yinsin.jpabatis.executor.ExecutorException;
import com.yinsin.jpabatis.mapper.BoundSql;
import com.yinsin.jpabatis.mapper.MappedStatement;
import com.yinsin.jpabatis.reflection.MetaObject;
import com.yinsin.jpabatis.session.ResultHandler;
import com.yinsin.jpabatis.session.RowBounds;

/**
 * @author Clinton Begin
 * @author Franta Mejta
 */
public class ResultLoaderMap {

	private final Map<String, LoadPair> loaderMap = new HashMap<>();

	public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
		String upperFirst = getUppercaseFirstProperty(property);
		if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
			throw new ExecutorException("Nested lazy loaded result property '" + property + "' for query id '" + resultLoader.mappedStatement.getId()
					+ " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
		}
		loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
	}

	public final Map<String, LoadPair> getProperties() {
		return new HashMap<>(this.loaderMap);
	}

	public Set<String> getPropertyNames() {
		return loaderMap.keySet();
	}

	public int size() {
		return loaderMap.size();
	}

	public boolean hasLoader(String property) {
		return loaderMap.containsKey(property.toUpperCase(Locale.ENGLISH));
	}

	public boolean load(String property) throws SQLException {
		LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
		if (pair != null) {
			pair.load();
			return true;
		}
		return false;
	}

	public void remove(String property) {
		loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
	}

	public void loadAll() throws SQLException {
		final Set<String> methodNameSet = loaderMap.keySet();
		String[] methodNames = methodNameSet.toArray(new String[methodNameSet.size()]);
		for (String methodName : methodNames) {
			load(methodName);
		}
	}

	private static String getUppercaseFirstProperty(String property) {
		String[] parts = property.split("\\.");
		return parts[0].toUpperCase(Locale.ENGLISH);
	}

	/**
	 * Property which was not loaded yet.
	 */
	public static class LoadPair implements Serializable {

		private static final long serialVersionUID = 20130412;
		/**
		 * Name of factory method which returns database connection.
		 */
		private static final String FACTORY_METHOD = "getConfiguration";
		/**
		 * Object to check whether we went through serialization..
		 */
		private final transient Object serializationCheck = new Object();
		/**
		 * Meta object which sets loaded properties.
		 */
		private transient MetaObject metaResultObject;
		/**
		 * Result loader which loads unread properties.
		 */
		private transient ResultLoader resultLoader;
		/**
		 * Wow, logger.
		 */
		private transient Logger log;
		/**
		 * Factory class through which we get database connection.
		 */
		private Class<?> configurationFactory;
		/**
		 * Name of the unread property.
		 */
		private String property;
		/**
		 * ID of SQL statement which loads the property.
		 */
		private String mappedStatement;
		/**
		 * Parameter of the sql statement.
		 */
		private Serializable mappedParameter;

		private LoadPair(final String property, MetaObject metaResultObject, ResultLoader resultLoader) {
			this.property = property;
			this.metaResultObject = metaResultObject;
			this.resultLoader = resultLoader;

			/*
			 * Save required information only if original object can be
			 * serialized.
			 */
			if (metaResultObject != null && metaResultObject.getOriginalObject() instanceof Serializable) {
				final Object mappedStatementParameter = resultLoader.parameterObject;

				/* @todo May the parameter be null? */
				if (mappedStatementParameter instanceof Serializable) {
					this.mappedStatement = resultLoader.mappedStatement.getId();
					this.mappedParameter = (Serializable) mappedStatementParameter;

					this.configurationFactory = resultLoader.configuration.getConfigurationFactory();
				} else {
					Logger log = this.getLogger();
					if (log.isDebugEnabled()) {
						log.debug("Property [" + this.property + "] of [" + metaResultObject.getOriginalObject().getClass() + "] cannot be loaded "
								+ "after deserialization. Make sure it's loaded before serializing " + "forenamed object.");
					}
				}
			}
		}

		public void load() throws SQLException {
			/*
			 * These field should not be null unless the loadpair was
			 * serialized. Yet in that case this method should not be called.
			 */
			if (this.metaResultObject == null) {
				throw new IllegalArgumentException("metaResultObject is null");
			}
			if (this.resultLoader == null) {
				throw new IllegalArgumentException("resultLoader is null");
			}

			this.load(null);
		}

		public void load(final Object userObject) throws SQLException {
			if (this.metaResultObject == null || this.resultLoader == null) {
				if (this.mappedParameter == null) {
					throw new ExecutorException("Property [" + this.property + "] cannot be loaded because " + "required parameter of mapped statement [" + this.mappedStatement
							+ "] is not serializable.");
				}

				final Configuration config = this.getConfiguration();
				final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
				if (ms == null) {
					throw new ExecutorException("Cannot lazy load property [" + this.property + "] of deserialized object [" + userObject.getClass()
							+ "] because configuration does not contain statement [" + this.mappedStatement + "]");
				}

				this.metaResultObject = config.newMetaObject(userObject);
				this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter, metaResultObject.getSetterType(this.property), null, null);
			}

			/*
			 * We are using a new executor because we may be (and likely are) on
			 * a new thread and executors aren't thread safe. (Is this
			 * sufficient?)
			 * 
			 * A better approach would be making executors thread safe.
			 */
			if (this.serializationCheck == null) {
				final ResultLoader old = this.resultLoader;
				this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement, old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
			}

			this.metaResultObject.setValue(property, this.resultLoader.loadResult());
		}

		private Configuration getConfiguration() {
			if (this.configurationFactory == null) {
				throw new ExecutorException("Cannot get Configuration as configuration factory was not set.");
			}

			Object configurationObject = null;
			try {
				final Method factoryMethod = this.configurationFactory.getDeclaredMethod(FACTORY_METHOD);
				if (!Modifier.isStatic(factoryMethod.getModifiers())) {
					throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#[" + FACTORY_METHOD + "] is not static.");
				}

				if (!factoryMethod.isAccessible()) {
					configurationObject = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
						@Override
						public Object run() throws Exception {
							try {
								factoryMethod.setAccessible(true);
								return factoryMethod.invoke(null);
							} finally {
								factoryMethod.setAccessible(false);
							}
						}
					});
				} else {
					configurationObject = factoryMethod.invoke(null);
				}
			} catch (final ExecutorException ex) {
				throw ex;
			} catch (final NoSuchMethodException ex) {
				throw new ExecutorException("Cannot get Configuration as factory class [" + this.configurationFactory + "] is missing factory method of name [" + FACTORY_METHOD
						+ "].", ex);
			} catch (final PrivilegedActionException ex) {
				throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#[" + FACTORY_METHOD + "] threw an exception.",
						ex.getCause());
			} catch (final Exception ex) {
				throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#[" + FACTORY_METHOD + "] threw an exception.", ex);
			}

			if (!(configurationObject instanceof Configuration)) {
				throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#[" + FACTORY_METHOD + "] didn't return ["
						+ Configuration.class + "] but [" + (configurationObject == null ? "null" : configurationObject.getClass()) + "].");
			}

			return Configuration.class.cast(configurationObject);
		}

		private Logger getLogger() {
			if (this.log == null) {
				this.log = LoggerFactory.getLogger(this.getClass());
			}
			return this.log;
		}
	}

	private static final class ClosedExecutor extends BaseExecutor {

		public ClosedExecutor() {
			super(null, null);
		}

		@Override
		public boolean isClose() {
			return true;
		}

		@Override
		public <E> E doQuery(MappedStatement ms, Object parameter, ResultHandler<?> resultHandler, BoundSql boundSql) throws SQLException {
			// TODO Auto-generated method stub
			return null;
		}

		@Override
		public <E> List<E> doQueryList(MappedStatement ms, Object parameter, ResultHandler<?> resultHandler, BoundSql boundSql) throws SQLException {
			// TODO Auto-generated method stub
			return null;
		}

		@Override
		public <E> Page<E> doQueryList(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler<?> resultHandler, BoundSql boundSql) throws SQLException {
			// TODO Auto-generated method stub
			return null;
		}

	}
}
